#!/usr/bin/env python3
"""Reorder the files in a directory to sort numerically and lexigraphically the
same.

People don't sort numbers lexigraphically (e.g. 10.jpg should not come before
2.jpg).
This means that a sort order on files with names containing numbers seems to be
one way to people and another to a computer.

This script attempts to rectify this by renaming the files in a directory to
sort the same numerically and lexigraphically. This is accomplished by making
all filenames:

    - A consistent length.
    - Numbers only.
    - A buffer in the number to allow for easy inserting later without
      requiring a complete renaming of the files.

"""
import os.path

def human_key(filename) -> float:
    """Return the found number in the filename.

    The obvious case should work.
    >>> human_key('1.jpg')
    1.0

    If a string of letter prefixes the number they should be ignored.
    >>> human_key('img-1.jpg')
    1.0

    A suffix of letters should be ignored as well.
    >> human_key('1-img.jpg')
    1.0

    Floats are also supported.
    >>> human_key('1.5.jpg')
    1.5

    """
    name = os.path.splitext(filename)[0]
    start = 0
    end = 0
    for index, character in enumerate(name):
        if character.isdigit():
            start = index
            break
    else:
        raise ValueError("no digits found in filename", repr(filename))
    for index, character in enumerate(name[start:]):
        if not character.isdigit() and character != '.':
            end = index + start
            break
    else:
        end = len(name)
    return float(name[start:end])


def reorder(directory):
    """Reorder the files in the directory based on the numerical substring in
    the filenames."""
    filenames = [x for x in os.listdir(directory) if not x.startswith('.')]
    filenames.sort(key=human_key)
    return filenames


def rename(sorted_names):
    """Rename the filenames to meet the desired requirements for the
    renaming:

    A trailing zero.
    >>> rename(['1.jpg', '2.jpg'])
    ['10.jpg', '20.jpg']

    '10' is the smallest filename.
    >>> rename(['0.jpg'])
    ['10.jpg']

    Name length is long enough to not change.
    >>> rename([str(x)+'.jpg' for x in range(10)])
    ['010.jpg', '020.jpg', '030.jpg', '040.jpg', '050.jpg', '060.jpg', '070.jpg', '080.jpg', '090.jpg', '100.jpg']

    Prepend an 'X' on name collisions.
    >>> rename(['00.jpg', '10.jpg'])
    ['X10.jpg', '20.jpg']

    """
    digits_needed = len(str(len(sorted_names)))
    names_set = set(sorted_names)
    new_names = []
    for index, name in enumerate(sorted_names):
        new_name = str(index+1).zfill(digits_needed)
        ext = os.path.splitext(name)[1]
        new_name = '{name}0{ext}'.format(name=new_name, ext=ext)
        if new_name in names_set:
            new_name = 'X' + new_name
        new_names.append(new_name)
    return new_names


def main(args):
    if len(args) != 1:
        raise ValueError("expect only a single argument")
    directory = args[0]
    if not os.path.isdir(directory):
        raise ValueError("expected an existing directory")
    old_names = reorder(directory)
    new_names = rename(old_names)
    for old_name, new_name in zip(old_names, new_names):
        print(old_name, '->', new_name)
    answer = input('OK (y/n)? ')
    if answer != 'y':
        print("Nothing changed")
        return
    for old_name, new_name in zip(old_names, new_names):
        os.rename(os.path.join(directory, old_name),
                    os.path.join(directory, new_name))
    print("Renamed")


if __name__ == '__main__':
    import sys
    main(sys.argv[1:])
